import {
  Bone,
  BufferAttribute,
  BufferGeometry,
  Color,
  FileLoader,
  Loader,
  LoaderUtils,
  Matrix4,
  Mesh,
  MeshLambertMaterial,
  MeshPhongMaterial,
  Object3D,
  Quaternion,
  Skeleton,
  SkinnedMesh,
  TextureLoader,
  Vector3,
} from 'three'

class AssimpLoader extends Loader {
  load(url, onLoad, onProgress, onError) {
    var scope = this

    var path = scope.path === '' ? LoaderUtils.extractUrlBase(url) : scope.path

    var loader = new FileLoader(scope.manager)
    loader.setPath(scope.path)
    loader.setResponseType('arraybuffer')
    loader.setRequestHeader(scope.requestHeader)
    loader.setWithCredentials(scope.withCredentials)

    loader.load(
      url,
      function (buffer) {
        try {
          onLoad(scope.parse(buffer, path))
        } catch (e) {
          if (onError) {
            onError(e)
          } else {
            console.error(e)
          }

          scope.manager.itemError(url)
        }
      },
      onProgress,
      onError,
    )
  }

  parse(buffer, path) {
    var textureLoader = new TextureLoader(this.manager)
    textureLoader.setPath(this.resourcePath || path).setCrossOrigin(this.crossOrigin)

    var Virtulous = {}

    Virtulous.KeyFrame = class {
      constructor(time, matrix) {
        this.time = time
        this.matrix = matrix.clone()
        this.position = new Vector3()
        this.quaternion = new Quaternion()
        this.scale = new Vector3(1, 1, 1)
        this.matrix.decompose(this.position, this.quaternion, this.scale)
        this.clone = function () {
          var n = new Virtulous.KeyFrame(this.time, this.matrix)
          return n
        }

        this.lerp = function (nextKey, time) {
          time -= this.time
          var dist = nextKey.time - this.time
          var l = time / dist
          var l2 = 1 - l
          var keypos = this.position
          var keyrot = this.quaternion
          //      var keyscl =  key.parentspaceScl || key.scl;
          var key2pos = nextKey.position
          var key2rot = nextKey.quaternion
          //  var key2scl =  key2.parentspaceScl || key2.scl;
          Virtulous.KeyFrame.tempAniPos.x = keypos.x * l2 + key2pos.x * l
          Virtulous.KeyFrame.tempAniPos.y = keypos.y * l2 + key2pos.y * l
          Virtulous.KeyFrame.tempAniPos.z = keypos.z * l2 + key2pos.z * l
          //     tempAniScale.x = keyscl[0] * l2 + key2scl[0] * l;
          //     tempAniScale.y = keyscl[1] * l2 + key2scl[1] * l;
          //     tempAniScale.z = keyscl[2] * l2 + key2scl[2] * l;
          Virtulous.KeyFrame.tempAniQuat.set(keyrot.x, keyrot.y, keyrot.z, keyrot.w)
          Virtulous.KeyFrame.tempAniQuat.slerp(key2rot, l)
          return Virtulous.KeyFrame.tempAniMatrix.compose(
            Virtulous.KeyFrame.tempAniPos,
            Virtulous.KeyFrame.tempAniQuat,
            Virtulous.KeyFrame.tempAniScale,
          )
        }
      }
    }

    Virtulous.KeyFrame.tempAniPos = new Vector3()
    Virtulous.KeyFrame.tempAniQuat = new Quaternion()
    Virtulous.KeyFrame.tempAniScale = new Vector3(1, 1, 1)
    Virtulous.KeyFrame.tempAniMatrix = new Matrix4()
    Virtulous.KeyFrameTrack = function () {
      this.keys = []
      this.target = null
      this.time = 0
      this.length = 0
      this._accelTable = {}
      this.fps = 20
      this.addKey = function (key) {
        this.keys.push(key)
      }

      this.init = function () {
        this.sortKeys()

        if (this.keys.length > 0) this.length = this.keys[this.keys.length - 1].time
        else this.length = 0

        if (!this.fps) return

        for (let j = 0; j < this.length * this.fps; j++) {
          for (let i = 0; i < this.keys.length; i++) {
            if (this.keys[i].time == j) {
              this._accelTable[j] = i
              break
            } else if (this.keys[i].time < j / this.fps && this.keys[i + 1] && this.keys[i + 1].time >= j / this.fps) {
              this._accelTable[j] = i
              break
            }
          }
        }
      }

      this.parseFromThree = function (data) {
        var fps = data.fps
        this.target = data.node
        var track = data.hierarchy[0].keys
        for (let i = 0; i < track.length; i++) {
          this.addKey(new Virtulous.KeyFrame(i / fps || track[i].time, track[i].targets[0].data))
        }

        this.init()
      }

      this.parseFromCollada = function (data) {
        var track = data.keys
        var fps = this.fps

        for (let i = 0; i < track.length; i++) {
          this.addKey(new Virtulous.KeyFrame(i / fps || track[i].time, track[i].matrix))
        }

        this.init()
      }

      this.sortKeys = function () {
        this.keys.sort(this.keySortFunc)
      }

      this.keySortFunc = function (a, b) {
        return a.time - b.time
      }

      this.clone = function () {
        var t = new Virtulous.KeyFrameTrack()
        t.target = this.target
        t.time = this.time
        t.length = this.length

        for (let i = 0; i < this.keys.length; i++) {
          t.addKey(this.keys[i].clone())
        }

        t.init()
        return t
      }

      this.reTarget = function (root, compareitor) {
        if (!compareitor) compareitor = Virtulous.TrackTargetNodeNameCompare
        this.target = compareitor(root, this.target)
      }

      this.keySearchAccel = function (time) {
        time *= this.fps
        time = Math.floor(time)
        return this._accelTable[time] || 0
      }

      this.setTime = function (time) {
        time = Math.abs(time)
        if (this.length) time = (time % this.length) + 0.05
        var key0 = null
        var key1 = null

        for (let i = this.keySearchAccel(time); i < this.keys.length; i++) {
          if (this.keys[i].time == time) {
            key0 = this.keys[i]
            key1 = this.keys[i]
            break
          } else if (this.keys[i].time < time && this.keys[i + 1] && this.keys[i + 1].time > time) {
            key0 = this.keys[i]
            key1 = this.keys[i + 1]
            break
          } else if (this.keys[i].time < time && i == this.keys.length - 1) {
            key0 = this.keys[i]
            key1 = this.keys[0].clone()
            key1.time += this.length + 0.05
            break
          }
        }

        if (key0 && key1 && key0 !== key1) {
          this.target.matrixAutoUpdate = false
          this.target.matrix.copy(key0.lerp(key1, time))
          this.target.matrixWorldNeedsUpdate = true
          return
        }

        if (key0 && key1 && key0 == key1) {
          this.target.matrixAutoUpdate = false
          this.target.matrix.copy(key0.matrix)
          this.target.matrixWorldNeedsUpdate = true
          return
        }
      }
    }

    Virtulous.TrackTargetNodeNameCompare = function (root, target) {
      function find(node, name) {
        if (node.name == name) return node

        for (let i = 0; i < node.children.length; i++) {
          var r = find(node.children[i], name)
          if (r) return r
        }

        return null
      }

      return find(root, target.name)
    }

    Virtulous.Animation = function () {
      this.tracks = []
      this.length = 0

      this.addTrack = function (track) {
        this.tracks.push(track)
        this.length = Math.max(track.length, this.length)
      }

      this.setTime = function (time) {
        this.time = time

        for (let i = 0; i < this.tracks.length; i++) this.tracks[i].setTime(time)
      }

      this.clone = function (target, compareitor) {
        if (!compareitor) compareitor = Virtulous.TrackTargetNodeNameCompare
        var n = new Virtulous.Animation()
        n.target = target
        for (let i = 0; i < this.tracks.length; i++) {
          var track = this.tracks[i].clone()
          track.reTarget(target, compareitor)
          n.addTrack(track)
        }

        return n
      }
    }

    var ASSBIN_CHUNK_AICAMERA = 0x1234
    var ASSBIN_CHUNK_AILIGHT = 0x1235
    var ASSBIN_CHUNK_AITEXTURE = 0x1236
    var ASSBIN_CHUNK_AIMESH = 0x1237
    var ASSBIN_CHUNK_AINODEANIM = 0x1238
    var ASSBIN_CHUNK_AISCENE = 0x1239
    var ASSBIN_CHUNK_AIBONE = 0x123a
    var ASSBIN_CHUNK_AIANIMATION = 0x123b
    var ASSBIN_CHUNK_AINODE = 0x123c
    var ASSBIN_CHUNK_AIMATERIAL = 0x123d
    var ASSBIN_CHUNK_AIMATERIALPROPERTY = 0x123e
    var ASSBIN_MESH_HAS_POSITIONS = 0x1
    var ASSBIN_MESH_HAS_NORMALS = 0x2
    var ASSBIN_MESH_HAS_TANGENTS_AND_BITANGENTS = 0x4
    var ASSBIN_MESH_HAS_TEXCOORD_BASE = 0x100
    var ASSBIN_MESH_HAS_COLOR_BASE = 0x10000
    var AI_MAX_NUMBER_OF_COLOR_SETS = 1
    var AI_MAX_NUMBER_OF_TEXTURECOORDS = 4
    //var aiLightSource_UNDEFINED = 0x0;
    //! A directional light source has a well-defined direction
    //! but is infinitely far away. That's quite a good
    //! approximation for sun light.
    var aiLightSource_DIRECTIONAL = 0x1
    //! A point light source has a well-defined position
    //! in space but no direction - it emits light in all
    //! directions. A normal bulb is a point light.
    //var aiLightSource_POINT = 0x2;
    //! A spot light source emits light in a specific
    //! angle. It has a position and a direction it is pointing to.
    //! A good example for a spot light is a light spot in
    //! sport arenas.
    var aiLightSource_SPOT = 0x3
    //! The generic light level of the world, including the bounces
    //! of all other lightsources.
    //! Typically, there's at most one ambient light in a scene.
    //! This light type doesn't have a valid position, direction, or
    //! other properties, just a color.
    //var aiLightSource_AMBIENT = 0x4;
    /** Flat shading. Shading is done on per-face base,
     *  diffuse only. Also known as 'faceted shading'.
     */
    //var aiShadingMode_Flat = 0x1;
    /** Simple Gouraud shading.
     */
    //var aiShadingMode_Gouraud = 0x2;
    /** Phong-Shading -
     */
    //var aiShadingMode_Phong = 0x3;
    /** Phong-Blinn-Shading
     */
    //var aiShadingMode_Blinn = 0x4;
    /** Toon-Shading per pixel
     *
     *  Also known as 'comic' shader.
     */
    //var aiShadingMode_Toon = 0x5;
    /** OrenNayar-Shading per pixel
     *
     *  Extension to standard Lambertian shading, taking the
     *  roughness of the material into account
     */
    //var aiShadingMode_OrenNayar = 0x6;
    /** Minnaert-Shading per pixel
     *
     *  Extension to standard Lambertian shading, taking the
     *  "darkness" of the material into account
     */
    //var aiShadingMode_Minnaert = 0x7;
    /** CookTorrance-Shading per pixel
     *
     *  Special shader for metallic surfaces.
     */
    //var aiShadingMode_CookTorrance = 0x8;
    /** No shading at all. Constant light influence of 1.0.
     */
    //var aiShadingMode_NoShading = 0x9;
    /** Fresnel shading
     */
    //var aiShadingMode_Fresnel = 0xa;
    //var aiTextureType_NONE = 0x0;
    /** The texture is combined with the result of the diffuse
     *  lighting equation.
     */
    var aiTextureType_DIFFUSE = 0x1
    /** The texture is combined with the result of the specular
     *  lighting equation.
     */
    //var aiTextureType_SPECULAR = 0x2;
    /** The texture is combined with the result of the ambient
     *  lighting equation.
     */
    //var aiTextureType_AMBIENT = 0x3;
    /** The texture is added to the result of the lighting
     *  calculation. It isn't influenced by incoming light.
     */
    //var aiTextureType_EMISSIVE = 0x4;
    /** The texture is a height map.
     *
     *  By convention, higher gray-scale values stand for
     *  higher elevations from the base height.
     */
    //var aiTextureType_HEIGHT = 0x5;
    /** The texture is a (tangent space) normal-map.
     *
     *  Again, there are several conventions for tangent-space
     *  normal maps. Assimp does (intentionally) not
     *  distinguish here.
     */
    var aiTextureType_NORMALS = 0x6
    /** The texture defines the glossiness of the material.
     *
     *  The glossiness is in fact the exponent of the specular
     *  (phong) lighting equation. Usually there is a conversion
     *  function defined to map the linear color values in the
     *  texture to a suitable exponent. Have fun.
     */
    //var aiTextureType_SHININESS = 0x7;
    /** The texture defines per-pixel opacity.
     *
     *  Usually 'white' means opaque and 'black' means
     *  'transparency'. Or quite the opposite. Have fun.
     */
    var aiTextureType_OPACITY = 0x8
    /** Displacement texture
     *
     *  The exact purpose and format is application-dependent.
     *  Higher color values stand for higher vertex displacements.
     */
    //var aiTextureType_DISPLACEMENT = 0x9;
    /** Lightmap texture (aka Ambient Occlusion)
     *
     *  Both 'Lightmaps' and dedicated 'ambient occlusion maps' are
     *  covered by this material property. The texture contains a
     *  scaling value for the final color value of a pixel. Its
     *  intensity is not affected by incoming light.
     */
    var aiTextureType_LIGHTMAP = 0xa
    /** Reflection texture
     *
     * Contains the color of a perfect mirror reflection.
     * Rarely used, almost never for real-time applications.
     */
    //var aiTextureType_REFLECTION = 0xB;
    /** Unknown texture
     *
     *  A texture reference that does not match any of the definitions
     *  above is considered to be 'unknown'. It is still imported,
     *  but is excluded from any further postprocessing.
     */
    //var aiTextureType_UNKNOWN = 0xC;
    var BONESPERVERT = 4

    function ASSBIN_MESH_HAS_TEXCOORD(n) {
      return ASSBIN_MESH_HAS_TEXCOORD_BASE << n
    }

    function ASSBIN_MESH_HAS_COLOR(n) {
      return ASSBIN_MESH_HAS_COLOR_BASE << n
    }

    function markBones(scene) {
      for (let i in scene.mMeshes) {
        var mesh = scene.mMeshes[i]
        for (let k in mesh.mBones) {
          var boneNode = scene.findNode(mesh.mBones[k].mName)
          if (boneNode) boneNode.isBone = true
        }
      }
    }

    function cloneTreeToBones(root, scene) {
      var rootBone = new Bone()
      rootBone.matrix.copy(root.matrix)
      rootBone.matrixWorld.copy(root.matrixWorld)
      rootBone.position.copy(root.position)
      rootBone.quaternion.copy(root.quaternion)
      rootBone.scale.copy(root.scale)
      scene.nodeCount++
      rootBone.name = 'bone_' + root.name + scene.nodeCount.toString()

      if (!scene.nodeToBoneMap[root.name]) scene.nodeToBoneMap[root.name] = []
      scene.nodeToBoneMap[root.name].push(rootBone)
      for (let i in root.children) {
        var child = cloneTreeToBones(root.children[i], scene)
        rootBone.add(child)
      }

      return rootBone
    }

    function sortWeights(indexes, weights) {
      var pairs = []

      for (let i = 0; i < indexes.length; i++) {
        pairs.push({
          i: indexes[i],
          w: weights[i],
        })
      }

      pairs.sort(function (a, b) {
        return b.w - a.w
      })

      while (pairs.length < 4) {
        pairs.push({
          i: 0,
          w: 0,
        })
      }

      if (pairs.length > 4) pairs.length = 4
      var sum = 0

      for (let i = 0; i < 4; i++) {
        sum += pairs[i].w * pairs[i].w
      }

      sum = Math.sqrt(sum)

      for (let i = 0; i < 4; i++) {
        pairs[i].w = pairs[i].w / sum
        indexes[i] = pairs[i].i
        weights[i] = pairs[i].w
      }
    }

    function findMatchingBone(root, name) {
      if (root.name.indexOf('bone_' + name) == 0) return root

      for (let i in root.children) {
        var ret = findMatchingBone(root.children[i], name)

        if (ret) return ret
      }

      return undefined
    }

    class aiMesh {
      constructor() {
        this.mPrimitiveTypes = 0
        this.mNumVertices = 0
        this.mNumFaces = 0
        this.mNumBones = 0
        this.mMaterialIndex = 0
        this.mVertices = []
        this.mNormals = []
        this.mTangents = []
        this.mBitangents = []
        this.mColors = [[]]
        this.mTextureCoords = [[]]
        this.mFaces = []
        this.mBones = []
        this.hookupSkeletons = function (scene) {
          if (this.mBones.length == 0) return

          var allBones = []
          var offsetMatrix = []
          var skeletonRoot = scene.findNode(this.mBones[0].mName)

          while (skeletonRoot.mParent && skeletonRoot.mParent.isBone) {
            skeletonRoot = skeletonRoot.mParent
          }

          var threeSkeletonRoot = skeletonRoot.toTHREE(scene)
          var threeSkeletonRootBone = cloneTreeToBones(threeSkeletonRoot, scene)
          this.threeNode.add(threeSkeletonRootBone)

          for (let i = 0; i < this.mBones.length; i++) {
            var bone = findMatchingBone(threeSkeletonRootBone, this.mBones[i].mName)

            if (bone) {
              var tbone = bone
              allBones.push(tbone)
              //tbone.matrixAutoUpdate = false;
              offsetMatrix.push(this.mBones[i].mOffsetMatrix.toTHREE())
            } else {
              var skeletonRoot = scene.findNode(this.mBones[i].mName)
              if (!skeletonRoot) return
              var threeSkeletonRoot = skeletonRoot.toTHREE(scene)
              var threeSkeletonRootBone = cloneTreeToBones(threeSkeletonRoot, scene)
              this.threeNode.add(threeSkeletonRootBone)
              var bone = findMatchingBone(threeSkeletonRootBone, this.mBones[i].mName)
              var tbone = bone
              allBones.push(tbone)
              //tbone.matrixAutoUpdate = false;
              offsetMatrix.push(this.mBones[i].mOffsetMatrix.toTHREE())
            }
          }

          var skeleton = new Skeleton(allBones, offsetMatrix)

          this.threeNode.bind(skeleton, new Matrix4())
          this.threeNode.material.skinning = true
        }

        this.toTHREE = function (scene) {
          if (this.threeNode) return this.threeNode
          var geometry = new BufferGeometry()
          var mat
          if (scene.mMaterials[this.mMaterialIndex]) mat = scene.mMaterials[this.mMaterialIndex].toTHREE(scene)
          else mat = new MeshLambertMaterial()
          geometry.setIndex(new BufferAttribute(new Uint32Array(this.mIndexArray), 1))
          geometry.setAttribute('position', new BufferAttribute(this.mVertexBuffer, 3))
          if (this.mNormalBuffer && this.mNormalBuffer.length > 0) {
            geometry.setAttribute('normal', new BufferAttribute(this.mNormalBuffer, 3))
          }
          if (this.mColorBuffer && this.mColorBuffer.length > 0) {
            geometry.setAttribute('color', new BufferAttribute(this.mColorBuffer, 4))
          }
          if (this.mTexCoordsBuffers[0] && this.mTexCoordsBuffers[0].length > 0) {
            geometry.setAttribute('uv', new BufferAttribute(new Float32Array(this.mTexCoordsBuffers[0]), 2))
          }
          if (this.mTexCoordsBuffers[1] && this.mTexCoordsBuffers[1].length > 0) {
            geometry.setAttribute('uv1', new BufferAttribute(new Float32Array(this.mTexCoordsBuffers[1]), 2))
          }
          if (this.mTangentBuffer && this.mTangentBuffer.length > 0) {
            geometry.setAttribute('tangents', new BufferAttribute(this.mTangentBuffer, 3))
          }
          if (this.mBitangentBuffer && this.mBitangentBuffer.length > 0) {
            geometry.setAttribute('bitangents', new BufferAttribute(this.mBitangentBuffer, 3))
          }
          if (this.mBones.length > 0) {
            var weights = []
            var bones = []

            for (let i = 0; i < this.mBones.length; i++) {
              for (let j = 0; j < this.mBones[i].mWeights.length; j++) {
                var weight = this.mBones[i].mWeights[j]
                if (weight) {
                  if (!weights[weight.mVertexId]) weights[weight.mVertexId] = []
                  if (!bones[weight.mVertexId]) bones[weight.mVertexId] = []
                  weights[weight.mVertexId].push(weight.mWeight)
                  bones[weight.mVertexId].push(parseInt(i))
                }
              }
            }

            for (let i in bones) {
              sortWeights(bones[i], weights[i])
            }

            var _weights = []
            var _bones = []

            for (let i = 0; i < weights.length; i++) {
              for (let j = 0; j < 4; j++) {
                if (weights[i] && bones[i]) {
                  _weights.push(weights[i][j])
                  _bones.push(bones[i][j])
                } else {
                  _weights.push(0)
                  _bones.push(0)
                }
              }
            }

            geometry.setAttribute('skinWeight', new BufferAttribute(new Float32Array(_weights), BONESPERVERT))
            geometry.setAttribute('skinIndex', new BufferAttribute(new Float32Array(_bones), BONESPERVERT))
          }

          var mesh

          if (this.mBones.length == 0) mesh = new Mesh(geometry, mat)

          if (this.mBones.length > 0) {
            mesh = new SkinnedMesh(geometry, mat)
            mesh.normalizeSkinWeights()
          }

          this.threeNode = mesh
          //mesh.matrixAutoUpdate = false;
          return mesh
        }
      }
    }

    class aiFace {
      constructor() {
        this.mNumIndices = 0
        this.mIndices = []
      }
    }

    class aiVector3D {
      constructor() {
        this.x = 0
        this.y = 0
        this.z = 0

        this.toTHREE = function () {
          return new Vector3(this.x, this.y, this.z)
        }
      }
    }

    class aiColor3D {
      constructor() {
        this.r = 0
        this.g = 0
        this.b = 0
        this.a = 0
        this.toTHREE = function () {
          return new Color(this.r, this.g, this.b)
        }
      }
    }

    class aiQuaternion {
      constructor() {
        this.x = 0
        this.y = 0
        this.z = 0
        this.w = 0
        this.toTHREE = function () {
          return new Quaternion(this.x, this.y, this.z, this.w)
        }
      }
    }

    class aiVertexWeight {
      constructor() {
        this.mVertexId = 0
        this.mWeight = 0
      }
    }

    class aiString {
      constructor() {
        this.data = []
        this.toString = function () {
          var str = ''
          this.data.forEach(function (i) {
            str += String.fromCharCode(i)
          })
          return str.replace(/[^\x20-\x7E]+/g, '')
        }
      }
    }

    class aiVectorKey {
      constructor() {
        this.mTime = 0
        this.mValue = null
      }
    }

    class aiQuatKey {
      constructor() {
        this.mTime = 0
        this.mValue = null
      }
    }

    class aiNode {
      constructor() {
        this.mName = ''
        this.mTransformation = []
        this.mNumChildren = 0
        this.mNumMeshes = 0
        this.mMeshes = []
        this.mChildren = []
        this.toTHREE = function (scene) {
          if (this.threeNode) return this.threeNode
          var o = new Object3D()
          o.name = this.mName
          o.matrix = this.mTransformation.toTHREE()

          for (let i = 0; i < this.mChildren.length; i++) {
            o.add(this.mChildren[i].toTHREE(scene))
          }

          for (let i = 0; i < this.mMeshes.length; i++) {
            o.add(scene.mMeshes[this.mMeshes[i]].toTHREE(scene))
          }

          this.threeNode = o
          //o.matrixAutoUpdate = false;
          o.matrix.decompose(o.position, o.quaternion, o.scale)
          return o
        }
      }
    }

    class aiBone {
      constructor() {
        this.mName = ''
        this.mNumWeights = 0
        this.mOffsetMatrix = 0
      }
    }

    class aiMaterialProperty {
      constructor() {
        this.mKey = ''
        this.mSemantic = 0
        this.mIndex = 0
        this.mData = []
        this.mDataLength = 0
        this.mType = 0
        this.dataAsColor = function () {
          var array = new Uint8Array(this.mData).buffer
          var reader = new DataView(array)
          var r = reader.getFloat32(0, true)
          var g = reader.getFloat32(4, true)
          var b = reader.getFloat32(8, true)
          //var a = reader.getFloat32(12, true);
          return new Color(r, g, b)
        }

        this.dataAsFloat = function () {
          var array = new Uint8Array(this.mData).buffer
          var reader = new DataView(array)
          var r = reader.getFloat32(0, true)
          return r
        }

        this.dataAsBool = function () {
          var array = new Uint8Array(this.mData).buffer
          var reader = new DataView(array)
          var r = reader.getFloat32(0, true)
          return !!r
        }

        this.dataAsString = function () {
          var s = new aiString()
          s.data = this.mData
          return s.toString()
        }

        this.dataAsMap = function () {
          var s = new aiString()
          s.data = this.mData
          var path = s.toString()
          path = path.replace(/\\/g, '/')

          if (path.indexOf('/') != -1) {
            path = path.substr(path.lastIndexOf('/') + 1)
          }

          return textureLoader.load(path)
        }
      }
    }

    var namePropMapping = {
      '?mat.name': 'name',
      '$mat.shadingm': 'shading',
      '$mat.twosided': 'twoSided',
      '$mat.wireframe': 'wireframe',
      '$clr.ambient': 'ambient',
      '$clr.diffuse': 'color',
      '$clr.specular': 'specular',
      '$clr.emissive': 'emissive',
      '$clr.transparent': 'transparent',
      '$clr.reflective': 'reflect',
      '$mat.shininess': 'shininess',
      '$mat.reflectivity': 'reflectivity',
      '$mat.refracti': 'refraction',
      '$tex.file': 'map',
    }

    var nameTypeMapping = {
      '?mat.name': 'string',
      '$mat.shadingm': 'bool',
      '$mat.twosided': 'bool',
      '$mat.wireframe': 'bool',
      '$clr.ambient': 'color',
      '$clr.diffuse': 'color',
      '$clr.specular': 'color',
      '$clr.emissive': 'color',
      '$clr.transparent': 'color',
      '$clr.reflective': 'color',
      '$mat.shininess': 'float',
      '$mat.reflectivity': 'float',
      '$mat.refracti': 'float',
      '$tex.file': 'map',
    }

    class aiMaterial {
      constructor() {
        this.mNumAllocated = 0
        this.mNumProperties = 0
        this.mProperties = []
        this.toTHREE = function () {
          var mat = new MeshPhongMaterial()

          for (let i = 0; i < this.mProperties.length; i++) {
            if (nameTypeMapping[this.mProperties[i].mKey] == 'float') {
              mat[namePropMapping[this.mProperties[i].mKey]] = this.mProperties[i].dataAsFloat()
            }
            if (nameTypeMapping[this.mProperties[i].mKey] == 'color') {
              mat[namePropMapping[this.mProperties[i].mKey]] = this.mProperties[i].dataAsColor()
            }
            if (nameTypeMapping[this.mProperties[i].mKey] == 'bool') {
              mat[namePropMapping[this.mProperties[i].mKey]] = this.mProperties[i].dataAsBool()
            }
            if (nameTypeMapping[this.mProperties[i].mKey] == 'string') {
              mat[namePropMapping[this.mProperties[i].mKey]] = this.mProperties[i].dataAsString()
            }
            if (nameTypeMapping[this.mProperties[i].mKey] == 'map') {
              var prop = this.mProperties[i]
              if (prop.mSemantic == aiTextureType_DIFFUSE) mat.map = this.mProperties[i].dataAsMap()
              if (prop.mSemantic == aiTextureType_NORMALS) mat.normalMap = this.mProperties[i].dataAsMap()
              if (prop.mSemantic == aiTextureType_LIGHTMAP) mat.lightMap = this.mProperties[i].dataAsMap()
              if (prop.mSemantic == aiTextureType_OPACITY) mat.alphaMap = this.mProperties[i].dataAsMap()
            }
          }

          mat.ambient.r = 0.53
          mat.ambient.g = 0.53
          mat.ambient.b = 0.53
          mat.color.r = 1
          mat.color.g = 1
          mat.color.b = 1
          return mat
        }
      }
    }

    function veclerp(v1, v2, l) {
      var v = new Vector3()
      var lm1 = 1 - l
      v.x = v1.x * l + v2.x * lm1
      v.y = v1.y * l + v2.y * lm1
      v.z = v1.z * l + v2.z * lm1
      return v
    }

    function quatlerp(q1, q2, l) {
      return q1.clone().slerp(q2, 1 - l)
    }

    function sampleTrack(keys, time, lne, lerp) {
      if (keys.length == 1) return keys[0].mValue.toTHREE()

      var dist = Infinity
      var key = null
      var nextKey = null

      for (let i = 0; i < keys.length; i++) {
        var timeDist = Math.abs(keys[i].mTime - time)

        if (timeDist < dist && keys[i].mTime <= time) {
          dist = timeDist
          key = keys[i]
          nextKey = keys[i + 1]
        }
      }

      if (!key) {
        return null
      } else if (nextKey) {
        var dT = nextKey.mTime - key.mTime
        var T = key.mTime - time
        var l = T / dT

        return lerp(key.mValue.toTHREE(), nextKey.mValue.toTHREE(), l)
      } else {
        nextKey = keys[0].clone()
        nextKey.mTime += lne

        var dT = nextKey.mTime - key.mTime
        var T = key.mTime - time
        var l = T / dT

        return lerp(key.mValue.toTHREE(), nextKey.mValue.toTHREE(), l)
      }
    }

    class aiNodeAnim {
      constructor() {
        this.mNodeName = ''
        this.mNumPositionKeys = 0
        this.mNumRotationKeys = 0
        this.mNumScalingKeys = 0
        this.mPositionKeys = []
        this.mRotationKeys = []
        this.mScalingKeys = []
        this.mPreState = ''
        this.mPostState = ''
        this.init = function (tps) {
          if (!tps) tps = 1

          function t(t) {
            t.mTime /= tps
          }

          this.mPositionKeys.forEach(t)
          this.mRotationKeys.forEach(t)
          this.mScalingKeys.forEach(t)
        }

        this.sortKeys = function () {
          function comp(a, b) {
            return a.mTime - b.mTime
          }

          this.mPositionKeys.sort(comp)
          this.mRotationKeys.sort(comp)
          this.mScalingKeys.sort(comp)
        }

        this.getLength = function () {
          return Math.max(
            Math.max.apply(
              null,
              this.mPositionKeys.map(function (a) {
                return a.mTime
              }),
            ),
            Math.max.apply(
              null,
              this.mRotationKeys.map(function (a) {
                return a.mTime
              }),
            ),
            Math.max.apply(
              null,
              this.mScalingKeys.map(function (a) {
                return a.mTime
              }),
            ),
          )
        }

        this.toTHREE = function (o) {
          this.sortKeys()
          var length = this.getLength()
          var track = new Virtulous.KeyFrameTrack()

          for (let i = 0; i < length; i += 0.05) {
            var matrix = new Matrix4()
            var time = i
            var pos = sampleTrack(this.mPositionKeys, time, length, veclerp)
            var scale = sampleTrack(this.mScalingKeys, time, length, veclerp)
            var rotation = sampleTrack(this.mRotationKeys, time, length, quatlerp)
            matrix.compose(pos, rotation, scale)

            var key = new Virtulous.KeyFrame(time, matrix)
            track.addKey(key)
          }

          track.target = o.findNode(this.mNodeName).toTHREE()

          var tracks = [track]

          if (o.nodeToBoneMap[this.mNodeName]) {
            for (let i = 0; i < o.nodeToBoneMap[this.mNodeName].length; i++) {
              var t2 = track.clone()
              t2.target = o.nodeToBoneMap[this.mNodeName][i]
              tracks.push(t2)
            }
          }

          return tracks
        }
      }
    }

    class aiAnimation {
      constructor() {
        this.mName = ''
        this.mDuration = 0
        this.mTicksPerSecond = 0
        this.mNumChannels = 0
        this.mChannels = []
        this.toTHREE = function (root) {
          var animationHandle = new Virtulous.Animation()

          for (let i in this.mChannels) {
            this.mChannels[i].init(this.mTicksPerSecond)

            var tracks = this.mChannels[i].toTHREE(root)

            for (let j in tracks) {
              tracks[j].init()
              animationHandle.addTrack(tracks[j])
            }
          }

          animationHandle.length = Math.max.apply(
            null,
            animationHandle.tracks.map(function (e) {
              return e.length
            }),
          )
          return animationHandle
        }
      }
    }

    class aiTexture {
      constructor() {
        this.mWidth = 0
        this.mHeight = 0
        this.texAchFormatHint = []
        this.pcData = []
      }
    }

    class aiLight {
      constructor() {
        this.mName = ''
        this.mType = 0
        this.mAttenuationConstant = 0
        this.mAttenuationLinear = 0
        this.mAttenuationQuadratic = 0
        this.mAngleInnerCone = 0
        this.mAngleOuterCone = 0
        this.mColorDiffuse = null
        this.mColorSpecular = null
        this.mColorAmbient = null
      }
    }

    class aiCamera {
      constructor() {
        this.mName = ''
        this.mPosition = null
        this.mLookAt = null
        this.mUp = null
        this.mHorizontalFOV = 0
        this.mClipPlaneNear = 0
        this.mClipPlaneFar = 0
        this.mAspect = 0
      }
    }

    class aiScene {
      constructor() {
        this.versionMajor = 0
        this.versionMinor = 0
        this.versionRevision = 0
        this.compileFlags = 0
        this.mFlags = 0
        this.mNumMeshes = 0
        this.mNumMaterials = 0
        this.mNumAnimations = 0
        this.mNumTextures = 0
        this.mNumLights = 0
        this.mNumCameras = 0
        this.mRootNode = null
        this.mMeshes = []
        this.mMaterials = []
        this.mAnimations = []
        this.mLights = []
        this.mCameras = []
        this.nodeToBoneMap = {}
        this.findNode = function (name, root) {
          if (!root) {
            root = this.mRootNode
          }

          if (root.mName == name) {
            return root
          }

          for (let i = 0; i < root.mChildren.length; i++) {
            var ret = this.findNode(name, root.mChildren[i])
            if (ret) return ret
          }

          return null
        }

        this.toTHREE = function () {
          this.nodeCount = 0

          markBones(this)

          var o = this.mRootNode.toTHREE(this)

          for (let i in this.mMeshes) this.mMeshes[i].hookupSkeletons(this)

          if (this.mAnimations.length > 0) {
            var a = this.mAnimations[0].toTHREE(this)
          }

          return { object: o, animation: a }
        }
      }
    }

    class aiMatrix4 {
      constructor() {
        this.elements = [[], [], [], []]
        this.toTHREE = function () {
          var m = new Matrix4()

          for (let i = 0; i < 4; ++i) {
            for (let i2 = 0; i2 < 4; ++i2) {
              m.elements[i * 4 + i2] = this.elements[i2][i]
            }
          }

          return m
        }
      }
    }

    var littleEndian = true

    function readFloat(dataview) {
      var val = dataview.getFloat32(dataview.readOffset, littleEndian)
      dataview.readOffset += 4
      return val
    }

    function Read_double(dataview) {
      var val = dataview.getFloat64(dataview.readOffset, littleEndian)
      dataview.readOffset += 8
      return val
    }

    function Read_uint8_t(dataview) {
      var val = dataview.getUint8(dataview.readOffset)
      dataview.readOffset += 1
      return val
    }

    function Read_uint16_t(dataview) {
      var val = dataview.getUint16(dataview.readOffset, littleEndian)
      dataview.readOffset += 2
      return val
    }

    function Read_unsigned_int(dataview) {
      var val = dataview.getUint32(dataview.readOffset, littleEndian)
      dataview.readOffset += 4
      return val
    }

    function Read_uint32_t(dataview) {
      var val = dataview.getUint32(dataview.readOffset, littleEndian)
      dataview.readOffset += 4
      return val
    }

    function Read_aiVector3D(stream) {
      var v = new aiVector3D()
      v.x = readFloat(stream)
      v.y = readFloat(stream)
      v.z = readFloat(stream)
      return v
    }

    function Read_aiColor3D(stream) {
      var c = new aiColor3D()
      c.r = readFloat(stream)
      c.g = readFloat(stream)
      c.b = readFloat(stream)
      return c
    }

    function Read_aiQuaternion(stream) {
      var v = new aiQuaternion()
      v.w = readFloat(stream)
      v.x = readFloat(stream)
      v.y = readFloat(stream)
      v.z = readFloat(stream)
      return v
    }

    function Read_aiString(stream) {
      var s = new aiString()
      var stringlengthbytes = Read_unsigned_int(stream)
      stream.ReadBytes(s.data, 1, stringlengthbytes)
      return s.toString()
    }

    function Read_aiVertexWeight(stream) {
      var w = new aiVertexWeight()
      w.mVertexId = Read_unsigned_int(stream)
      w.mWeight = readFloat(stream)
      return w
    }

    function Read_aiMatrix4x4(stream) {
      var m = new aiMatrix4()

      for (let i = 0; i < 4; ++i) {
        for (let i2 = 0; i2 < 4; ++i2) {
          m.elements[i][i2] = readFloat(stream)
        }
      }

      return m
    }

    function Read_aiVectorKey(stream) {
      var v = new aiVectorKey()
      v.mTime = Read_double(stream)
      v.mValue = Read_aiVector3D(stream)
      return v
    }

    function Read_aiQuatKey(stream) {
      var v = new aiQuatKey()
      v.mTime = Read_double(stream)
      v.mValue = Read_aiQuaternion(stream)
      return v
    }

    function ReadArray_aiVertexWeight(stream, data, size) {
      for (let i = 0; i < size; i++) data[i] = Read_aiVertexWeight(stream)
    }

    function ReadArray_aiVectorKey(stream, data, size) {
      for (let i = 0; i < size; i++) data[i] = Read_aiVectorKey(stream)
    }

    function ReadArray_aiQuatKey(stream, data, size) {
      for (let i = 0; i < size; i++) data[i] = Read_aiQuatKey(stream)
    }

    function ReadBounds(stream, T /*p*/, n) {
      // not sure what to do here, the data isn't really useful.
      return stream.Seek(sizeof(T) * n, aiOrigin_CUR)
    }

    function ai_assert(bool) {
      if (!bool) throw 'asset failed'
    }

    function ReadBinaryNode(stream, parent, depth) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AINODE)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      var node = new aiNode()
      node.mParent = parent
      node.mDepth = depth
      node.mName = Read_aiString(stream)
      node.mTransformation = Read_aiMatrix4x4(stream)
      node.mNumChildren = Read_unsigned_int(stream)
      node.mNumMeshes = Read_unsigned_int(stream)

      if (node.mNumMeshes) {
        node.mMeshes = []

        for (let i = 0; i < node.mNumMeshes; ++i) {
          node.mMeshes[i] = Read_unsigned_int(stream)
        }
      }

      if (node.mNumChildren) {
        node.mChildren = []

        for (let i = 0; i < node.mNumChildren; ++i) {
          var node2 = ReadBinaryNode(stream, node, depth++)
          node.mChildren[i] = node2
        }
      }

      return node
    }

    // -----------------------------------------------------------------------------------

    function ReadBinaryBone(stream, b) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AIBONE)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      b.mName = Read_aiString(stream)
      b.mNumWeights = Read_unsigned_int(stream)
      b.mOffsetMatrix = Read_aiMatrix4x4(stream)
      // for the moment we write dumb min/max values for the bones, too.
      // maybe I'll add a better, hash-like solution later
      if (shortened) {
        ReadBounds(stream, b.mWeights, b.mNumWeights)
      } else {
        // else write as usual

        b.mWeights = []
        ReadArray_aiVertexWeight(stream, b.mWeights, b.mNumWeights)
      }

      return b
    }

    function ReadBinaryMesh(stream, mesh) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AIMESH)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      mesh.mPrimitiveTypes = Read_unsigned_int(stream)
      mesh.mNumVertices = Read_unsigned_int(stream)
      mesh.mNumFaces = Read_unsigned_int(stream)
      mesh.mNumBones = Read_unsigned_int(stream)
      mesh.mMaterialIndex = Read_unsigned_int(stream)
      mesh.mNumUVComponents = []
      // first of all, write bits for all existent vertex components
      var c = Read_unsigned_int(stream)

      if (c & ASSBIN_MESH_HAS_POSITIONS) {
        if (shortened) {
          ReadBounds(stream, mesh.mVertices, mesh.mNumVertices)
        } else {
          // else write as usual

          mesh.mVertices = []
          mesh.mVertexBuffer = stream.subArray32(stream.readOffset, stream.readOffset + mesh.mNumVertices * 3 * 4)
          stream.Seek(mesh.mNumVertices * 3 * 4, aiOrigin_CUR)
        }
      }

      if (c & ASSBIN_MESH_HAS_NORMALS) {
        if (shortened) {
          ReadBounds(stream, mesh.mNormals, mesh.mNumVertices)
        } else {
          // else write as usual

          mesh.mNormals = []
          mesh.mNormalBuffer = stream.subArray32(stream.readOffset, stream.readOffset + mesh.mNumVertices * 3 * 4)
          stream.Seek(mesh.mNumVertices * 3 * 4, aiOrigin_CUR)
        }
      }

      if (c & ASSBIN_MESH_HAS_TANGENTS_AND_BITANGENTS) {
        if (shortened) {
          ReadBounds(stream, mesh.mTangents, mesh.mNumVertices)
          ReadBounds(stream, mesh.mBitangents, mesh.mNumVertices)
        } else {
          // else write as usual

          mesh.mTangents = []
          mesh.mTangentBuffer = stream.subArray32(stream.readOffset, stream.readOffset + mesh.mNumVertices * 3 * 4)
          stream.Seek(mesh.mNumVertices * 3 * 4, aiOrigin_CUR)
          mesh.mBitangents = []
          mesh.mBitangentBuffer = stream.subArray32(stream.readOffset, stream.readOffset + mesh.mNumVertices * 3 * 4)
          stream.Seek(mesh.mNumVertices * 3 * 4, aiOrigin_CUR)
        }
      }

      for (let n = 0; n < AI_MAX_NUMBER_OF_COLOR_SETS; ++n) {
        if (!(c & ASSBIN_MESH_HAS_COLOR(n))) break

        if (shortened) {
          ReadBounds(stream, mesh.mColors[n], mesh.mNumVertices)
        } else {
          // else write as usual

          mesh.mColors[n] = []
          mesh.mColorBuffer = stream.subArray32(stream.readOffset, stream.readOffset + mesh.mNumVertices * 4 * 4)
          stream.Seek(mesh.mNumVertices * 4 * 4, aiOrigin_CUR)
        }
      }

      mesh.mTexCoordsBuffers = []

      for (let n = 0; n < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++n) {
        if (!(c & ASSBIN_MESH_HAS_TEXCOORD(n))) break

        // write number of UV components
        mesh.mNumUVComponents[n] = Read_unsigned_int(stream)

        if (shortened) {
          ReadBounds(stream, mesh.mTextureCoords[n], mesh.mNumVertices)
        } else {
          // else write as usual

          mesh.mTextureCoords[n] = []
          //note that assbin always writes 3d texcoords
          mesh.mTexCoordsBuffers[n] = []

          for (let uv = 0; uv < mesh.mNumVertices; uv++) {
            mesh.mTexCoordsBuffers[n].push(readFloat(stream))
            mesh.mTexCoordsBuffers[n].push(readFloat(stream))
            readFloat(stream)
          }
        }
      }

      // write faces. There are no floating-point calculations involved
      // in these, so we can write a simple hash over the face data
      // to the dump file. We generate a single 32 Bit hash for 512 faces
      // using Assimp's standard hashing function.
      if (shortened) {
        Read_unsigned_int(stream)
      } else {
        // else write as usual

        // if there are less than 2^16 vertices, we can simply use 16 bit integers ...
        mesh.mFaces = []
        mesh.mIndexArray = []

        for (let i = 0; i < mesh.mNumFaces; ++i) {
          var f = (mesh.mFaces[i] = new aiFace())
          // BOOST_STATIC_ASSERT(AI_MAX_FACE_INDICES <= 0xffff);
          f.mNumIndices = Read_uint16_t(stream)
          f.mIndices = []

          for (let a = 0; a < f.mNumIndices; ++a) {
            if (mesh.mNumVertices < 1 << 16) {
              f.mIndices[a] = Read_uint16_t(stream)
            } else {
              f.mIndices[a] = Read_unsigned_int(stream)
            }
          }

          if (f.mNumIndices === 3) {
            mesh.mIndexArray.push(f.mIndices[0])
            mesh.mIndexArray.push(f.mIndices[1])
            mesh.mIndexArray.push(f.mIndices[2])
          } else if (f.mNumIndices === 4) {
            mesh.mIndexArray.push(f.mIndices[0])
            mesh.mIndexArray.push(f.mIndices[1])
            mesh.mIndexArray.push(f.mIndices[2])
            mesh.mIndexArray.push(f.mIndices[2])
            mesh.mIndexArray.push(f.mIndices[3])
            mesh.mIndexArray.push(f.mIndices[0])
          } else {
            throw new Error("Sorry, can't currently triangulate polys. Use the triangulate preprocessor in Assimp.")
          }
        }
      }

      // write bones
      if (mesh.mNumBones) {
        mesh.mBones = []

        for (let a = 0; a < mesh.mNumBones; ++a) {
          mesh.mBones[a] = new aiBone()
          ReadBinaryBone(stream, mesh.mBones[a])
        }
      }
    }

    function ReadBinaryMaterialProperty(stream, prop) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AIMATERIALPROPERTY)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      prop.mKey = Read_aiString(stream)
      prop.mSemantic = Read_unsigned_int(stream)
      prop.mIndex = Read_unsigned_int(stream)
      prop.mDataLength = Read_unsigned_int(stream)
      prop.mType = Read_unsigned_int(stream)
      prop.mData = []
      stream.ReadBytes(prop.mData, 1, prop.mDataLength)
    }

    // -----------------------------------------------------------------------------------

    function ReadBinaryMaterial(stream, mat) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AIMATERIAL)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      mat.mNumAllocated = mat.mNumProperties = Read_unsigned_int(stream)

      if (mat.mNumProperties) {
        if (mat.mProperties) {
          delete mat.mProperties
        }

        mat.mProperties = []

        for (let i = 0; i < mat.mNumProperties; ++i) {
          mat.mProperties[i] = new aiMaterialProperty()
          ReadBinaryMaterialProperty(stream, mat.mProperties[i])
        }
      }
    }

    function ReadBinaryNodeAnim(stream, nd) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AINODEANIM)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      nd.mNodeName = Read_aiString(stream)
      nd.mNumPositionKeys = Read_unsigned_int(stream)
      nd.mNumRotationKeys = Read_unsigned_int(stream)
      nd.mNumScalingKeys = Read_unsigned_int(stream)
      nd.mPreState = Read_unsigned_int(stream)
      nd.mPostState = Read_unsigned_int(stream)

      if (nd.mNumPositionKeys) {
        if (shortened) {
          ReadBounds(stream, nd.mPositionKeys, nd.mNumPositionKeys)
        } else {
          // else write as usual

          nd.mPositionKeys = []
          ReadArray_aiVectorKey(stream, nd.mPositionKeys, nd.mNumPositionKeys)
        }
      }

      if (nd.mNumRotationKeys) {
        if (shortened) {
          ReadBounds(stream, nd.mRotationKeys, nd.mNumRotationKeys)
        } else {
          // else write as usual

          nd.mRotationKeys = []
          ReadArray_aiQuatKey(stream, nd.mRotationKeys, nd.mNumRotationKeys)
        }
      }

      if (nd.mNumScalingKeys) {
        if (shortened) {
          ReadBounds(stream, nd.mScalingKeys, nd.mNumScalingKeys)
        } else {
          // else write as usual

          nd.mScalingKeys = []
          ReadArray_aiVectorKey(stream, nd.mScalingKeys, nd.mNumScalingKeys)
        }
      }
    }

    function ReadBinaryAnim(stream, anim) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AIANIMATION)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      anim.mName = Read_aiString(stream)
      anim.mDuration = Read_double(stream)
      anim.mTicksPerSecond = Read_double(stream)
      anim.mNumChannels = Read_unsigned_int(stream)

      if (anim.mNumChannels) {
        anim.mChannels = []

        for (let a = 0; a < anim.mNumChannels; ++a) {
          anim.mChannels[a] = new aiNodeAnim()
          ReadBinaryNodeAnim(stream, anim.mChannels[a])
        }
      }
    }

    function ReadBinaryTexture(stream, tex) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AITEXTURE)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      tex.mWidth = Read_unsigned_int(stream)
      tex.mHeight = Read_unsigned_int(stream)
      stream.ReadBytes(tex.achFormatHint, 1, 4)

      if (!shortened) {
        if (!tex.mHeight) {
          tex.pcData = []
          stream.ReadBytes(tex.pcData, 1, tex.mWidth)
        } else {
          tex.pcData = []
          stream.ReadBytes(tex.pcData, 1, tex.mWidth * tex.mHeight * 4)
        }
      }
    }

    function ReadBinaryLight(stream, l) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AILIGHT)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      l.mName = Read_aiString(stream)
      l.mType = Read_unsigned_int(stream)

      if (l.mType != aiLightSource_DIRECTIONAL) {
        l.mAttenuationConstant = readFloat(stream)
        l.mAttenuationLinear = readFloat(stream)
        l.mAttenuationQuadratic = readFloat(stream)
      }

      l.mColorDiffuse = Read_aiColor3D(stream)
      l.mColorSpecular = Read_aiColor3D(stream)
      l.mColorAmbient = Read_aiColor3D(stream)

      if (l.mType == aiLightSource_SPOT) {
        l.mAngleInnerCone = readFloat(stream)
        l.mAngleOuterCone = readFloat(stream)
      }
    }

    function ReadBinaryCamera(stream, cam) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AICAMERA)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      cam.mName = Read_aiString(stream)
      cam.mPosition = Read_aiVector3D(stream)
      cam.mLookAt = Read_aiVector3D(stream)
      cam.mUp = Read_aiVector3D(stream)
      cam.mHorizontalFOV = readFloat(stream)
      cam.mClipPlaneNear = readFloat(stream)
      cam.mClipPlaneFar = readFloat(stream)
      cam.mAspect = readFloat(stream)
    }

    function ReadBinaryScene(stream, scene) {
      var chunkID = Read_uint32_t(stream)
      ai_assert(chunkID == ASSBIN_CHUNK_AISCENE)
      /*uint32_t size =*/
      Read_uint32_t(stream)
      scene.mFlags = Read_unsigned_int(stream)
      scene.mNumMeshes = Read_unsigned_int(stream)
      scene.mNumMaterials = Read_unsigned_int(stream)
      scene.mNumAnimations = Read_unsigned_int(stream)
      scene.mNumTextures = Read_unsigned_int(stream)
      scene.mNumLights = Read_unsigned_int(stream)
      scene.mNumCameras = Read_unsigned_int(stream)
      // Read node graph
      scene.mRootNode = new aiNode()
      scene.mRootNode = ReadBinaryNode(stream, null, 0)
      // Read all meshes
      if (scene.mNumMeshes) {
        scene.mMeshes = []

        for (let i = 0; i < scene.mNumMeshes; ++i) {
          scene.mMeshes[i] = new aiMesh()
          ReadBinaryMesh(stream, scene.mMeshes[i])
        }
      }

      // Read materials
      if (scene.mNumMaterials) {
        scene.mMaterials = []

        for (let i = 0; i < scene.mNumMaterials; ++i) {
          scene.mMaterials[i] = new aiMaterial()
          ReadBinaryMaterial(stream, scene.mMaterials[i])
        }
      }

      // Read all animations
      if (scene.mNumAnimations) {
        scene.mAnimations = []

        for (let i = 0; i < scene.mNumAnimations; ++i) {
          scene.mAnimations[i] = new aiAnimation()
          ReadBinaryAnim(stream, scene.mAnimations[i])
        }
      }

      // Read all textures
      if (scene.mNumTextures) {
        scene.mTextures = []

        for (let i = 0; i < scene.mNumTextures; ++i) {
          scene.mTextures[i] = new aiTexture()
          ReadBinaryTexture(stream, scene.mTextures[i])
        }
      }

      // Read lights
      if (scene.mNumLights) {
        scene.mLights = []

        for (let i = 0; i < scene.mNumLights; ++i) {
          scene.mLights[i] = new aiLight()
          ReadBinaryLight(stream, scene.mLights[i])
        }
      }

      // Read cameras
      if (scene.mNumCameras) {
        scene.mCameras = []

        for (let i = 0; i < scene.mNumCameras; ++i) {
          scene.mCameras[i] = new aiCamera()
          ReadBinaryCamera(stream, scene.mCameras[i])
        }
      }
    }

    var aiOrigin_CUR = 0
    var aiOrigin_BEG = 1

    function extendStream(stream) {
      stream.readOffset = 0
      stream.Seek = function (off, ori) {
        if (ori == aiOrigin_CUR) {
          stream.readOffset += off
        }

        if (ori == aiOrigin_BEG) {
          stream.readOffset = off
        }
      }

      stream.ReadBytes = function (buff, size, n) {
        var bytes = size * n
        for (let i = 0; i < bytes; i++) buff[i] = Read_uint8_t(this)
      }

      stream.subArray32 = function (start, end) {
        var buff = this.buffer
        var newbuff = buff.slice(start, end)
        return new Float32Array(newbuff)
      }

      stream.subArrayUint16 = function (start, end) {
        var buff = this.buffer
        var newbuff = buff.slice(start, end)
        return new Uint16Array(newbuff)
      }

      stream.subArrayUint8 = function (start, end) {
        var buff = this.buffer
        var newbuff = buff.slice(start, end)
        return new Uint8Array(newbuff)
      }

      stream.subArrayUint32 = function (start, end) {
        var buff = this.buffer
        var newbuff = buff.slice(start, end)
        return new Uint32Array(newbuff)
      }
    }

    var shortened, compressed

    function InternReadFile(pFiledata) {
      var pScene = new aiScene()
      var stream = new DataView(pFiledata)
      extendStream(stream)
      stream.Seek(44, aiOrigin_CUR) // signature
      /*unsigned int versionMajor =*/
      pScene.versionMajor = Read_unsigned_int(stream)
      /*unsigned int versionMinor =*/
      pScene.versionMinor = Read_unsigned_int(stream)
      /*unsigned int versionRevision =*/
      pScene.versionRevision = Read_unsigned_int(stream)
      /*unsigned int compileFlags =*/
      pScene.compileFlags = Read_unsigned_int(stream)
      shortened = Read_uint16_t(stream) > 0
      compressed = Read_uint16_t(stream) > 0
      if (shortened) throw 'Shortened binaries are not supported!'
      stream.Seek(256, aiOrigin_CUR) // original filename
      stream.Seek(128, aiOrigin_CUR) // options
      stream.Seek(64, aiOrigin_CUR) // padding
      if (compressed) {
        var uncompressedSize = Read_uint32_t(stream)
        var compressedSize = stream.FileSize() - stream.Tell()
        var compressedData = []
        stream.Read(compressedData, 1, compressedSize)
        var uncompressedData = []
        uncompress(uncompressedData, uncompressedSize, compressedData, compressedSize)
        var buff = new ArrayBuffer(uncompressedData)
        ReadBinaryScene(buff, pScene)
      } else {
        ReadBinaryScene(stream, pScene)
      }

      return pScene.toTHREE()
    }

    return InternReadFile(buffer)
  }
}

export { AssimpLoader }
